<?php

/**
 * SvcPHP
 *
 * Copyright (C) 2012 João Francisco Biondo Trinca
 *
 * Permission is hereby granted, free of charge, to any person
 * obtaining a copy of this software and associated documentation
 * files (the "Software"), to deal in the Software without restriction,
 * including without limitation the rights to use, copy, modify, merge,
 * publish, distribute, sublicense, and/or sell copies of the Software,
 * and to permit persons to whom the Software is furnished to do so,
 * subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
 * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
 * IN THE SOFTWARE.
 *
 * @package    SvcPHP
 * @author     João Francisco Biondo Trinca <wolfulus@gmail.com>
 * @copyright  2012 João Francisco Biondo Trinca <wolfulus@gmail.com>
 * @license    http://www.opensource.org/licenses/mit-license.html  MIT License
 * @link       http://code.google.com/p/svcphp/
 *
 */

require_once('Svc/Functions.php');
require_once('Svc/Request.php');
require_once('Svc/Response.php');
require_once('Svc/Controller.php');
require_once('Svc/SessionManager.php');
require_once('Svc/CookieManager.php');
require_once('Svc/Translator.php');
require_once('Svc/Template/Data.php');
require_once('Svc/Template/Renderer.php');
require_once('Svc/Template.php');
require_once('Svc/Exception.php');
require_once('Svc/Orm.php');
require_once('Svc/Paris.php');

/**
 * Application class implementation.
 */
class Svc_Application
{
    /**
     * Application instance.
     *
     * @var mixed
     */
    private static $_instance = null;

    /**
     * Stores the error codes.
     *
     * @var mixed
     */
    private $_errors = array(404 => "Not Found", 500 => "Internal Error", 501 => "Not Implemented");

    /**
     * Stores the application path.
     *
     * @var mixed
     */
    private $_path = '.';

    /**
     * Stores the session manager instance.
     *
     * @var Svc_SessionManager
     */
    private $_session = null;

    /**
     * Stores the cookie manager instance.
     *
     * @var Svc_CookieManager
     */
    private $_cookies = null;

    /**
     * Stores the translator instance.
     *
     * @var Svc_Translator
     */
    private $_translator = null;

    /**
     * Stores the template instance.
     *
     * @var Svc_Template
     */
    private $_template = null;

    /**
     * Stores if the application is using a rewrite engine.
     *
     * @var boolean
     */
    private $_rewrite = false;

    /**
     * Stores the key used for navigation when not in SEO friendly mode.
     *
     * @var string
     */
    private $_pageKey = 'view';

    /**
     * Stores the filters
     *
     * @var Svc_Filter
     */
    private $_filters = array();

    /**
     * Creates an application instance.
     *
     * @param $path string
     * @param bool $rewrite
     * @param string $pageKey
     * @param string $cacheDir
     *
     * @internal param mixed $verbose
     *
     * @return \Svc_Application
     */
    public function __construct($path = '.', $rewrite = true, $pageKey = 'view', $cacheDir = "cache/templates")
    {
        Svc_Application::$_instance = & $this;

        $this->_rewrite = $rewrite;
        $this->_pageKey = $pageKey;

        $path = realpath($path);
        if ($path == false)
        {
            die("Application folder not found.");
        }

        $pathLast = substr($path, -1, 1) == '/';
        if ($pathLast == '/' || $pathLast == "\\")
        {
            $path = substr($path, 0, strlen($path) - 1);
        }

        $this->_path = $path;

        set_include_path(get_include_path() . PATH_SEPARATOR . realpath($this->_path . DIRECTORY_SEPARATOR . "classes") . PATH_SEPARATOR . realpath($this->_path . DIRECTORY_SEPARATOR . "models") . PATH_SEPARATOR);

        spl_autoload_register("__svc_autoload", false);

        $this->_cookies = new Svc_CookieManager();
        $this->_session = new Svc_SessionManager();
        $this->_session->start();

        $this->_translator = new Svc_Translator();
        $this->_template = new Svc_Template(array('view_dir' => $this->getPath("views"), 'cache_dir' => $this->getPath($cacheDir), 'cache_timeout' => 3600, 'file_extension' => 'html',));

        $this->_template->registerPlugin(new Svc_Template_Plugin_Translator());
        $this->_template->registerPlugin(new Svc_Template_Plugin_Url());
        $this->_template->registerPlugin(new Svc_Template_Plugin_Time());
        $this->_template->registerPlugin(new Svc_Template_Plugin_Gravatar());
    }

    /**
     * Adds a filter to the application
     *
     * @param mixed $filter
     */
    public function addFilter($filter)
    {
        $this->_filters[] = $filter;
    }

    /**
     * Adds a filter to the application
     *
     * @param $plugin
     *
     * @internal param mixed $filter
     */
    public function addPlugin($plugin)
    {
        $this->_template->registerPlugin($plugin);
    }

    /**
     * Gets if the application is using a rewrite engine.
     *
     * @return bool
     */
    public function isUsingRewrite()
    {
        return $this->_rewrite;
    }

    /**
     * Gets if the application is using a rewrite engine.
     *
     * @return string
     */
    public function getNavigationKey()
    {
        return $this->_pageKey;
    }

    /**
     * Gets the application instance.
     *
     * @return Svc_Application
     */
    public static function & instance()
    {
        return Svc_Application::$_instance;
    }

    /**
     * Gets the session manager instance.
     *
     * @return Svc_SessionManager
     */
    public function & getSession()
    {
        return $this->_session;
    }

    /**
     * Gets the cookie manager.
     *
     * @return Svc_CookieManager
     */
    public function & getCookies()
    {
        return $this->_cookies;
    }

    /**
     * Gets the translator.
     *
     * @return Svc_Translator
     */
    public function & getTranslator()
    {
        return $this->_translator;
    }

    /**
     * Gets the current template engine.
     *
     * @return Svc_Template
     */
    public function & getTemplate()
    {
        return $this->_template;
    }

    /**
     * Gets an application file path.
     *
     * @param string $file
     *
     * @return string
     */
    public function getPath($file)
    {
        $start = substr($file, 0, 1);
        if ($start == '\\' || $start == '/')
        {
            $file = substr($file, 1);
        }

        return $this->_path . DIRECTORY_SEPARATOR . $file;
    }

    /**
     * Parses the request URI.
     */
    private function parseRequest()
    {
        $uri = parse_url($_SERVER['REQUEST_URI']);

        if (!$this->isUsingRewrite())
        {
            $path = Svc_Request::getQuery($this->_pageKey, 'index/index');
        }
        else
        {
            if (isset($_SERVER['REQUEST_URI']))
            {
                $path = $uri['path'];
            }
            else
            {
                $path = 'index/index';
            }
        }

        if ($path{0} == '/')
        {
            $path = substr($path, 1);
        }

        $path = array_filter(explode('/', $path), "__svc_notempty");

        $controller = 'index';
        $action = 'index';
        $parameters = array();

        switch (sizeof($path))
        {
            case 0:
                break;
            case 1:
                $controller = $path[0];
                break;
            case 2:
                $controller = $path[0];
                $action = $path[1];
                break;
            default:
                $controller = $path[0];
                $action = $path[1];
                for ($i = 2; $i < sizeof($path); $i++)
                {
                    $parameters[] = $path[$i];
                }
                break;
        }

        return array('controller' => $controller, 'action' => $action, 'parameters' => $parameters);
    }

    /**
     * Runs the application
     *
     */
    public function run($verbose = false)
    {
        $result = null;
        $exception = null;

        try
        {
            $result = $this->dispatch();
            foreach ($result['headers'] as $header)
            {
                header($header);
            }

            echo $result['output'];

            if ($verbose && !empty($result['logs']))
            {
                echo '<pre style="background-color: #FFE4B5; clear: both;">';
                echo $result['logs'];
                echo '</pre>';
            }

            return;
        }
        catch (Exception $ex)
        {
            $exception = $ex;
        }

        if ($exception !== null)
        {
            if ($exception instanceof Svc_Exception_NotFound)
            {
                $code = 404;
            }
            else if ($exception instanceof Svc_Exception_NotImplemented)
            {
                $code = 501;
            }
            else
            {
                $code = 500;
            }

            $message = $exception->getMessage();

            if (isset($this->_errors[$code]))
            {
                header('HTTP 1.1 ' . $code . ' ' . (isset($this->_errors[$code]) ? $this->_errors[$code] : ''));
            }
            else
            {
                header('Status: ' . $code);
            }

            $this->_template->clear();
            $this->_template->assign('code', $code);
            $this->_template->assign('message', $message);

            try
            {
                $output = $this->_template->render('errors/' . $code);
            }
            catch (Exception $exception)
            {
                header("HTTP 1.1 500 " . $this->_errors[500]);
                echo $exception->getMessage();
                exit;
            }

            $this->_template->clear();
            $this->_template->assign('content', $output);

            try
            {
                $output = $this->_template->render('errors/error');
            }
            catch (Exception $exception)
            {
                header("HTTP 1.1 500 " . $this->_errors[500]);
                echo $exception->getMessage();
                exit;
            }

            echo $output;
        }
    }

    /**
     * Runs the application.
     *
     * @throws Svc_Exception_ServerError
     * @throws Svc_Exception_NotFound
     * @throws Svc_Exception_NotImplemented
     * @return void
     */
    private function dispatch()
    {
        $result = array('headers' => array(), 'output' => '', 'logs' => '');

        $data = $this->parseRequest();

        $controller = trim($data['controller']);
        if (is_null($controller) || empty($controller))
        {
            $controller = 'index';
        }

        $action = trim($data['action']);
        if (is_null($action) || empty($action))
        {
            $action = 'index';
        }

        $filterResult = null;
        $response = new Svc_Response();
        $request = new Svc_Request($controller, $action);
        foreach ($this->_filters as $filter)
        {
            $filterResult = $filter->filter($request, $response);
            if (is_resource($filterResult))
            {
                break;
            }
        }

        if ($filterResult !== null)
        {
            if ($filterResult instanceof Svc_Response_Redirect)
            {
                header('Location: ' . $filterResult->url());
                return $result;
            }
        }

        $controllerName = ucfirst($request->getControllerName() . 'Controller');
        $actionName = $request->getActionName() . ($request->isPost() ? 'ActionPost' : 'Action');

        if (!class_exists($controllerName, false))
        {
            $path = $this->getPath("controllers" . DIRECTORY_SEPARATOR . $controllerName . ".php");
            @include($path);
        }

        if (class_exists($controllerName, false))
        {
            /** @var Svc_Controller $instance */
            $instance = new $controllerName($this, $request, $response);
            if ($instance instanceof Svc_Controller)
            {
                if (method_exists($instance, $actionName))
                {
                    $parameters = $data['parameters'];

                    if ($request->isPost())
                    {
                        $form = new stdClass();
                        foreach ($_POST as $name => $value)
                        {
                            $form->$name = $value;
                        }

                        $parameters = array($form);
                        foreach ($data['parameters'] as $value)
                        {
                            $parameters[] = $value;
                        }
                    }

                    $reflection = new ReflectionMethod($controllerName, $actionName);
                    $params = $reflection->getNumberOfRequiredParameters();

                    if (sizeof($parameters) < $params)
                    {
                        for ($i = 0; $i < $params - sizeof($parameters); $i++)
                        {
                            array_push($parameters, null);
                        }
                    }

                    ob_start();
                    $instance->init();
                    $actionResult = call_user_func_array(array($instance, $actionName), $parameters);
                    $result['logs'] = ob_get_clean();

                    if ($actionResult instanceof Svc_Response_Redirect)
                    {
                        header('Location: ' . $actionResult->url());
                    }
                    else if ($actionResult instanceof Svc_Response_Json)
                    {
                        $result['headers'][] = 'Content-Type: text/json;';
                        $result['output'] = $actionResult->render();
                    }
                    else if ($actionResult instanceof Svc_Response_View)
                    {
                        $contentType = false;
                        $charset = false;

                        foreach ($instance->getResponse()->getHeaders() as $header => $value)
                        {
                            if (strtolower($header) == 'content-type')
                            {
                                $contentType = true;
                                if (strpos($value, "charset=") !== false)
                                {
                                    $charset = true;
                                    continue;
                                }
                            }
                            $result['headers'][] = $header . ': ' . $value;
                        }

                        if (!$charset)
                        {
                            if ($contentType !== false)
                            {
                                $temp = $instance->getResponse()->getHeaders();
                                $result['headers'][] = 'Content-Type: ' . $temp['Content-Type'] . '; charset=UTF-8';
                            }
                            else
                            {
                                $result['headers'][] = 'Content-Type: text/html; charset=UTF-8';
                            }
                        }

                        try
                        {
                            if ($actionResult->enabled())
                            {
                                $vars = get_object_vars($instance->getViewContent());
                                foreach ($vars as $name => $value)
                                {
                                    $this->_template->assign($name, $value);
                                }

                                $view = $actionResult->view();
                                if ($view !== null)
                                {
                                    $view = 'pages/' . $view;
                                }
                                else
                                {
                                    $view = 'pages/' . $request->getControllerName() . '/' . $request->getActionName();
                                }

                                try
                                {
                                    $result['output'] = $this->_template->render($view);
                                }
                                catch (Exception $ex)
                                {
                                    throw new Svc_Exception_ServerError($ex->getMessage());
                                }

                                $this->_template->clear();

                                $vars = get_object_vars($instance->getViewContent());
                                foreach ($vars as $name => $value)
                                {
                                    $this->_template->assign($name, $value);
                                }

                                $this->_template->assign('content', $result['output']);

                                $view = $actionResult->template();
                                try
                                {
                                    $result['output'] = $this->_template->render('pages/' . $view);
                                }
                                catch (Exception $ex)
                                {
                                    throw new Svc_Exception_ServerError($ex->getMessage());
                                }
                            }
                        }
                        catch (Exception $ex)
                        {
                            if (ob_get_level() > 0)
                            {
                                $result['logs'] .= ob_get_clean();
                            }

                            throw new Svc_Exception_ServerError($ex->getMessage());
                        }
                    }
                }
                else
                {
                    throw new Svc_Exception_NotImplemented();
                }
            }
            else
            {
                throw new Svc_Exception_NotImplemented();
            }
        }
        else
        {
            throw new Svc_Exception_NotFound();
        }

        return $result;
    }
}
